﻿using System;

namespace Sencha.Serialization.Bson
{
	/// <summary>
	/// Representation of raw binary data in BSON. This type is immutable by design
	/// </summary>
	[TypeSerializer(typeof(Serializers.BsonBinarySerializer))]
	public sealed class BsonBinary : IEquatable<BsonBinary>, IComparable, IComparable<BsonBinary>
	{
		private static readonly byte[] EmptyBytes = new byte[0];

		private byte m_subType;
		private byte[] m_binary;
		private int m_count;
		private int m_startIndex;

		internal byte[] Bytes { get { return m_binary; } }
		internal int Offset { get { return m_startIndex; } }		
		public int Count { get { return m_count; } }
		public byte SubType { get { return m_subType; } }
		public byte this[int index] { get { return m_binary[m_startIndex + index]; } }

		public BsonBinary()
		{
			m_subType = 0; // generic binary
			m_binary = EmptyBytes;
		}
		public BsonBinary(byte[] binaryData)
			: this(0, binaryData)
		{
		}
		public BsonBinary(byte subType, byte[] binaryData)
		{
			if (binaryData == null)
				throw new ArgumentNullException("binaryData");

			m_subType = subType;
			m_binary = binaryData;
			m_count = m_binary.Length;
		}
		public BsonBinary(byte subType, byte[] binaryData, int startIndex, int count)
		{
			if (binaryData == null)
				throw new ArgumentNullException("binaryData");
			if (startIndex < 0)
				throw new ArgumentOutOfRangeException("startIndex");
			if (count < 0)
				throw new ArgumentOutOfRangeException("count");
			if (startIndex + count > binaryData.Length)
				throw new ArgumentOutOfRangeException("startIndex and count");

			m_startIndex = startIndex;
			m_count = count;
			m_subType = subType;
			m_binary = binaryData;
		}
		public BsonBinary(Guid uuid)
		{
			m_subType = 2;
			m_binary = uuid.ToByteArray();
			m_count = m_binary.Length;
		}

		public void CopyTo(byte[] destination, int index, int bytesToCopy)
		{
			Buffer.BlockCopy(m_binary, m_startIndex, destination, index, Math.Min(bytesToCopy, m_count));
		}
		public byte[] ToByteArray()
		{
			var bytes = new byte[m_count];
			Buffer.BlockCopy(m_binary, m_startIndex, bytes, 0, bytes.Length);
			return bytes;
		}
		public string ToBase64()
		{
			return Convert.ToBase64String(m_binary, m_startIndex, m_count);
		}
		/// <summary>
		/// Converts byte[] to System.Guid
		/// </summary>
		/// <returns>new Syste.Guid</returns>
		/// <exception cref="System.ArgumentException">Binary is not 16 bytes long.</exception>
		public Guid ToGuid()
		{
			if (this.m_count != 16)
				return new Guid(m_binary);

			var uuidBytes = new byte[16];
			Buffer.BlockCopy(m_binary, m_startIndex, uuidBytes, 0, 16);

			return new Guid(uuidBytes);
		}


		#region IEquatable<BsonBinary> Members

		public override bool Equals(object obj)
		{
			return this.Equals(obj as BsonBinary);
		}

		public override int GetHashCode()
		{
			if (m_count >= 4)
				return m_binary.Length.GetHashCode() ^ BitConverter.ToInt32(m_binary, m_startIndex);

			return m_binary.Length.GetHashCode();
		}

		public bool Equals(BsonBinary other)
		{
			if (other == null)
				return false;
			if (ReferenceEquals(this, other))
				return true;

			if (this.m_count != other.m_count)
				return false;
			if (this.GetHashCode() != other.GetHashCode())
				return false;

			for (var i = 0; i < m_count; i++)
			{
				if (this[i] != other[i])
					return false;
			}
			return true;
		}

		#endregion
		#region IComparable Members

		public int CompareTo(object obj)
		{
			return this.CompareTo(obj as BsonBinary);
		}

		#endregion
		#region IComparable<BsonBinary> Members

		public int CompareTo(BsonBinary other)
		{
			if (other == null)
				return 1;

			// wee need to align buffers with different sizes
			for (int i = 0, j = 0; i < this.Count || j < other.Count; i++, j++)
			{
				// we need offsets
				var io = this.Count - other.Count;
				var jo = other.Count - this.Count;
				// only negative offset is needed
				if (io > 0) io = 0;
				if (jo > 0) jo = 0;

				// get bytes with offsets
				var ib = i + io >= 0 ? this[i + io] : 0;
				var jb = j + jo >= 0 ? other[j + jo] : 0;

				// compare
				if (ib > jb)
					return 1;
				else if (jb > ib)
					return -1;
			}

			return 0;
		}

		#endregion

		public static bool operator ==(BsonBinary a, BsonBinary b)
		{
			if (ReferenceEquals(a, null) && ReferenceEquals(b, null))
				return false;
			else if (ReferenceEquals(a, null) || ReferenceEquals(b, null))
				return false;
			else
				return a.Equals(b);

		}
		public static bool operator !=(BsonBinary a, BsonBinary b)
		{
			return !(a == b);
		}

		public override string ToString()
		{
			return string.Format("binary: count={0}", this.Count);
		}
	}
}
